/*
 * WebComponent.java
 *
 * Created on March 26, 2007, 6:04 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package org.atomojo.www;

import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import org.atomojo.app.client.Entry;
import org.atomojo.app.client.FeedClient;
import org.atomojo.app.client.FeedDestination;
import org.atomojo.app.client.Link;
import org.atomojo.app.client.Ontology;
import org.atomojo.app.client.Text;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.restlet.Client;
import org.restlet.Component;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.Server;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Status;

/**
 *
 * @author alex
 */
public class WebComponent extends Component {

   static final URI HOST_TERM = Ontology.getDefaultInstance().find(ConfiguredHost.T_BASE,"host").getURI();
   static final Name VIEWPORT = Name.create("{http://www.w3.org/2007/03/xproc}viewport");
   static final Name XSLT = Name.create("{http://www.w3.org/2007/03/xproc}xslt");
   static final Name INPUT = Name.create("{http://www.w3.org/2007/03/xproc}input");
   static final Name INLINE = Name.create("{http://www.w3.org/2007/03/xproc}inline");
   public static final String LINKS_ATTR = "org.atomojo.www.app.links";

   class AutoConfiguration {

      Configuration.Interface iface;
      Link link;
      Map<String, ConfiguredHost> hosts;
      Map<String, ConfiguredHost> confHosts;
      boolean changes;
      Client webClient;

      AutoConfiguration(Configuration.Interface iface, Link link) {
         this.iface = iface;
         this.link = link;
         this.hosts = new TreeMap<String, ConfiguredHost>();
         this.confHosts = ifaceHosts.get(iface.getKey());
         this.webClient = getContext().getClientDispatcher();
      }

      public void configure() {
         FeedClient client = new FeedClient(webClient,new Reference(link.getLink()));
         client.setIdentity(link.getUsername(), link.getPassword());

         getContext().getLogger().info("Loading configuration for " + iface.getAddress() + ":" + iface.getPort() + " from " + link.getLink());

         changes = false;
         final Map<String, Boolean> found = new TreeMap<String, Boolean>();
         try {
            client.get(new FeedDestination() {

               public void onFeed(Document feedDoc) {
               }

               public void onEntry(Document entryDoc) {
                  Entry entry = new Entry(entryDoc);
                  entry.index();
                  if (entry.getTerm(HOST_TERM) == null) {
                     return;
                  }
                  Text text = entry.getContent();
                  if (text == null || !text.isXML()) {
                     getContext().getLogger().warning("Ignoring host entry with missing or has incorrectly typed content for configuration.");
                     return;
                  }
                  Element hostE = text.getContent();
                  if (hostE == null) {
                     getContext().getLogger().warning("Ignoring host entry with empty content.");
                     return;
                  }

                  Date edited = null;

                  try {
                     edited = entry.getEdited();
                     if (edited == null) {
                        edited = entry.getUpdated();
                     }
                  } catch (ParseException ex) {
                     getContext().getLogger().warning("Ignoring host entry with malformed date in updated or edited element: " + ex.getMessage());
                     return;
                  }

                  if (edited == null) {
                     getContext().getLogger().warning("Ignoring host entry without updated or edited element.");
                     return;
                  }

                  found.put(entry.getId(), Boolean.TRUE);

                  ConfiguredHost autoHost = hosts.get(entry.getId());
                  if (autoHost==null) {
                     getLogger().info("New host detected in entry "+entry.getId());
                  } else if (autoHost.edited.getTime() < edited.getTime()) {
                     // the entry has changed, so delete
                     getContext().getLogger().info("Entry changed for host " + autoHost.getVirtualHost().getHostDomain() + ", reconfiguring.");
                     getHosts().remove(autoHost.getVirtualHost());
                     autoHost = null;
                  }

                  if (autoHost != null) {
                     autoHost.check();
                     return;
                  }

                  // Create configuration
                  changes = true;

                  try {
                     Configuration.Host host = config.createHost(hostE);

                     String hostName = host.getName();
                     if (hostName == null) {
                        getLogger().severe("Host entry " + entry.getId() + " does not have a host name attribute.");
                        return;
                     }
                     getLogger().info("Auto-configuring host " + host.getName());


                     for (Map<String, ConfiguredHost> confHosts : ifaceHosts.values()) {
                        if (confHosts.get(host.getName()) != null) {
                           getLogger().warning("Ignoring duplicate host for " + host.getName() + " from auto-configuration.");
                           return;
                        }
                     }

                     Context hostContext = getContext().createChildContext();
                     
                     ConfiguredHost confHost = new ConfiguredHost(hostContext, getInternalRouter(),iface, host, edited, false);

                     // The auto host should not be in confHosts as it isn't static
                     //confHosts.put(host.getName(), confHost);
                     hosts.put(entry.getId(), confHost);

                     getHosts().add(confHost.getVirtualHost());

                  } catch (XMLException ex) {
                     getLogger().log(Level.SEVERE, "Cannot load host configuration.", ex);
                  }
               }
            });

            // remove all entries that have been deleted
            List<String> toRemove = new ArrayList<String>();
            for (String id : hosts.keySet()) {
               if (found.get(id) == null) {
                  ConfiguredHost autoHost = hosts.get(id);
                  getLogger().info("Removing host " + autoHost.getVirtualHost().getHostDomain());
                  changes = true;
                  toRemove.add(id);
                  getHosts().remove(autoHost.getVirtualHost());
                  confHosts.remove(autoHost.getVirtualHost().getHostDomain());
               }
            }
            for (String id : toRemove) {
               hosts.remove(id);
            }

            if (changes && WebComponent.this.isStarted()) {
               getLogger().info("Updating hosts...");
               WebComponent.this.updateHosts();
               /*
               restarting = true;
               stop();
               start();
               restarting = false;
                */
            }

         } catch (Exception ex) {
            getContext().getLogger().log(Level.SEVERE, "Fatal error while getting auto configuration feed at " + link.getLink(), ex);
         }
         getContext().getLogger().info("Configuration " + link.getLink() + " loaded.");
      }
   }

   class AutoConfProcess implements Runnable {

      boolean run = true;

      public void run() {
         while (run) {
            try {
               for (AutoConfiguration autoconf : autoconfs) {
                  autoconf.configure();
                  if (!run) {
                     break;
                  }
               }
               getLogger().info("Waiting " + (config.getAutoConfigurationCheckWait() / 1000) + "s to check configuration.");
               Thread.currentThread().sleep(config.getAutoConfigurationCheckWait());
               for (Map<String, ConfiguredHost> confHosts : ifaceHosts.values()) {
                  for (ConfiguredHost confHost : confHosts.values()) {
                     if (confHost.isStatic()) {
                        confHost.check();
                     }
                  }
               }
            } catch (InterruptedException ex) {

            }

         }
         getLogger().info("Auto configuration thread exiting.");
      }
   }
   static public String LOG_NAME = "org.atomojo.hosts";
   Configuration config;
   Map<String, Map<String, ConfiguredHost>> ifaceHosts;
   List<AutoConfiguration> autoconfs;
   AutoConfProcess autoconfProc;
   Thread autoconfThread;
   boolean restarting = false;
   boolean hasStaticAutoConf = false;

   /** Creates a new instance of WebComponent */
   public WebComponent(Configuration config) {
      this.config = config;
      this.ifaceHosts = new TreeMap<String, Map<String, ConfiguredHost>>();
      this.autoconfs = new ArrayList<AutoConfiguration>();
      getLogService().setLoggerName(LOG_NAME);

      if (config.getKeyStorePath() != null) {
         getContext().getParameters().add("keystorePath", config.getKeyStorePath().getAbsolutePath());
         getContext().getParameters().add("keystorePassword", config.getKeyStorePassword());
         getContext().getParameters().add("keyPassword", config.getKeyStorePassword());
      }

      /*
      ScriptEngineManager manager = new ScriptEngineManager();
      for (ScriptEngineFactory factory : manager.getEngineFactories()) {
         StringBuilder w = new StringBuilder();
         w.append("\nLanguage Name: " + factory.getLanguageName());
         w.append("\nLanguage Version: " + factory.getLanguageVersion());
         w.append("\nEngine Name: " + factory.getEngineName());
         w.append("\nEngine Version: " + factory.getEngineVersion());
         w.append("\nExtensions:");
         for (String ext : factory.getExtensions()) {
            w.append(" " + ext);
         }
         w.append("\nMedia Types:");
         for (String type : factory.getMimeTypes()) {
            w.append(" " + type);
         }
         w.append("\n\n");
         getLogger().info("Script Engines: " + w.toString());
      }*/

      for (Configuration.Interface iface : config.getInterfaces()) {
         if (iface.isSecure()) {
            getContext().getLogger().info("https listening on " + iface.getAddress() + ":" + iface.getPort());
            Server server = getServers().add(Protocol.HTTPS, iface.getAddress().equals("*") ? null : iface.getAddress(), iface.getPort());
            if (config.getKeyStorePath() != null) {
               server.getContext().getParameters().add("keystorePath", config.getKeyStorePath().getAbsolutePath());
               server.getContext().getParameters().add("keystorePassword", config.getKeyStorePassword());
               server.getContext().getParameters().add("keyPassword", config.getKeyStorePassword());
            }
         } else {
            getContext().getLogger().info("http listening on " + iface.getAddress() + ":" + iface.getPort());
            getServers().add(Protocol.HTTP, iface.getAddress().equals("*") ? null : iface.getAddress(), iface.getPort());
         }

         Map<String, ConfiguredHost> confHosts = ifaceHosts.get(iface.getKey());
         if (confHosts == null) {
            confHosts = new TreeMap<String, ConfiguredHost>();
            ifaceHosts.put(iface.getKey(), confHosts);
         }
         // Configure static hosts
         for (String name : iface.getHosts().keySet()) {
            final Configuration.Host host = iface.getHosts().get(name);
            if (confHosts.get(host.getName()) != null) {
               getLogger().warning("Ignoring duplicate host name " + host.getName());
            } else {
               Context hostContext = getContext().createChildContext();
               
               ConfiguredHost confHost = new ConfiguredHost(hostContext, getInternalRouter(), iface, host, new Date(), true);
               confHosts.put(host.getName(), confHost);
               if (host.getLinks().get("autoconf") != null) {
                  hasStaticAutoConf = true;
               }

               getHosts().add(confHost.getVirtualHost());
            }

         }

         // Configure autoconf
         List<Link> autoFeeds = iface.getLinks().get("autoconf");
         if (autoFeeds != null) {
            for (Link link : autoFeeds) {
               addAutoConfiguration(iface, link);
            }
         }

      }
      this.getDefaultHost().attach(new Restlet() {

         public void handle(Request request, Response response) {
            response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         }
      });

      // Add the clients
      getClients().add(Protocol.FILE);
      getClients().add(Protocol.HTTP);
      getClients().add(Protocol.HTTPS);

      for (Server server : getServers()) {
         try {
            server.start();
         } catch (Exception ex) {
            getLogger().log(Level.SEVERE,"Cannot start server.",ex);
         }
      }

   }

   public void addAutoConfiguration(Configuration.Interface iface, Link link) {
      AutoConfiguration autoconf = new AutoConfiguration(iface, link);
      autoconfs.add(autoconf);
   }

   public void start()
      throws Exception 
   {
      super.start();
      if (!restarting) {
         if (autoconfs.size() > 0 || hasStaticAutoConf) {
            getLogger().info("Starting auto-configuration process...");
            autoconfProc = new AutoConfProcess();
            autoconfThread = new Thread(autoconfProc);
            autoconfThread.start();
            getLogger().info("...started.");
         } else {
            autoconfProc = null;
         }
      }
   }

   public void stop()
      throws Exception 
   {
      if (!restarting) {
         if (autoconfProc != null) {
            getLogger().info("Stopping auto-configuration process...");
            autoconfProc.run = false;
            synchronized (autoconfThread) {
               autoconfThread.interrupt();
            }
            autoconfThread.join(2000);
            getLogger().info("...stopped.");
         }
      }
      super.stop();
   }
}
